#include <string.h>
#include <glib.h>
#include "qemu/osdep.h"
#include "hw/irq.h"
#include "hw/qdev-properties.h"
#include "qemu/timer.h"
#include "sysemu/runstate.h"
#include "qemu/bitops.h"
#include "hw/sysbus.h"
#include "migration/vmstate.h"
#include "hw/arm/primecell.h"
#include "qemu/log.h"
#include "qemu/module.h"
#include "hw/ipmi/imu.h"
#include "qemu/fifo8.h"
#include "hw/misc/utils.h"
#include "qemu/error-report.h"


#define TYPE_HS0704_I2C "hs0704"
#define HS0704(obj) \
    OBJECT_CHECK(hs0704_state, (obj), TYPE_HS0704_I2C)

#ifdef DEBUG_HS0704
#define DPRINTF(_x ...) printf(_x)
#else
#define DPRINTF(_x ...)
#endif

#define FIFO_LENGTH         64

#define HS0704_DISABLE_SET_REG(i2c_status, target, value) \
{                                                             \
    do {                                                      \
        if ((i2c_status) == 0) {                              \
            (target) = (value);                               \
        }                                                     \
    } while (0);                                              \
}                                                             \

// flags of 0x10
#define IPMB_CTR_OFFSET_ADDR             0x0
#define IPMB_TRXR_OFFSET_ADDR            0x4
#define IPMB_CR_OFFSET_ADDR              0x8
#define IPMB_SR_OFFSET_ADDR              0xC
#define IPMB_FRM_LEN_OFFSET_ADDR         0x10
#define IPMB_RCV_LEN_OFFSET_ADDR         0x14
#define IPMB_TRANS_LEN_OFFSET_ADDR       0x18
#define IPMB_PREP_LEN_OFFSET_ADDR        0x20
#define IPMB_SLVADDR_A_OFFSET_ADDR        0x24
#define IPMB_SLVADDR_B_OFFSET_ADDR        0x28
#define IPMB_MTSR_TEST_OFFSET_ADDR        0x30
#define IPMB_DISCARD_CNT_OFFSET_ADDR        0x34
#define IPMB_RCV_CNT_OFFSET_ADDR        0x38
#define IPMB_TX_STATE_OFFSET_ADDR        0x40
#define IPMB_ECC_ERR_INT_OFFSET_ADDR        0x50
#define IPMB_START_TICK1_OFFSET_ADDR        0x60
#define IPMB_START_TICK2_OFFSET_ADDR        0x64
#define IPMB_STOP_TICK1_OFFSET_ADDR        0x68
#define IPMB_STOP_TICK2_OFFSET_ADDR        0x6C
#define IPMB_WR_TICK1_OFFSET_ADDR        0x7
#define IPMB_WR_TICK2_OFFSET_ADDR        0x74
#define IPMB_RD_TICK1_OFFSET_ADDR        0x78
#define IPMB_RD_TICK2_OFFSET_ADDR        0x7C
#define IPMB_IPMB_ID_OFFSET_ADDR        0xC10

#define BIT(nr)                 (1UL << (nr))

typedef struct {
    uint32_t ctr;
    uint32_t xr;
    uint32_t cr;
    uint32_t sr;
    uint32_t frm_len;
    uint32_t prer;
    uint32_t slvaddr_a;
    uint32_t slvaddr_b;
    uint32_t mstr_test;
    uint32_t discard_cnt;
    uint32_t rcv_cnt;
    uint32_t tx_state;
    uint32_t ecc_err_int;
    uint32_t start_tick1;
    uint32_t start_tick2;
    uint32_t stop_tick1;
    uint32_t stop_tick2;
    uint32_t wr_tick1;
    uint32_t wr_tick2;
    uint32_t rd_tick1;
    uint32_t rd_tick2;
    uint32_t ipmb_id;
} hs0704_regs;

typedef struct {
    SysBusDevice    parent_obj;
    MemoryRegion    iomem;
    qemu_irq        irq;
    /* IPMB控制器寄存器列表 */
    hs0704_regs regs;
    // rx fifo ====
	Fifo8       rx_fifo;
	Fifo8       tx_fifo;
	imu imu_info;

} hs0704_state;

enum {
    A_NEW_TRANS = 0,
    CONTINU_TRANS,
    SEND_DATA,
    READ_DATA,
};


static uint64_t hs0704_read_part0(hwaddr offset, hs0704_state *s)
{
    hs0704_regs *regs = &s->regs;
    uint32_t ret = 0;

    switch (offset) {
        case IPMB_CTR_OFFSET_ADDR:
            return regs->ctr;
        case IPMB_TRXR_OFFSET_ADDR:
            return fifo8_pop(&s->rx_fifo);
        case IPMB_CR_OFFSET_ADDR:
            return 0;
        case IPMB_SR_OFFSET_ADDR:
            return fifo8_is_empty(&s->rx_fifo) ? regs->sr:regs->sr|0x04;
        case IPMB_FRM_LEN_OFFSET_ADDR:
            return regs->frm_len;
        case IPMB_RCV_LEN_OFFSET_ADDR:
            return fifo8_num_used(&s->rx_fifo);
        case IPMB_TRANS_LEN_OFFSET_ADDR :
			return fifo8_num_used(&s->tx_fifo);
        default:
            return -1;
    }
    return ret;
}

static uint64_t hs0704_read_part1(hwaddr offset, hs0704_state *s)
{
    hs0704_regs *regs = &s->regs;
    uint32_t ret = 0;

    switch (offset) {
        case IPMB_PREP_LEN_OFFSET_ADDR:
            return regs->prer;
		case IPMB_SLVADDR_A_OFFSET_ADDR:
            return regs->slvaddr_a;
		case IPMB_SLVADDR_B_OFFSET_ADDR:
            return regs->slvaddr_b;
		case IPMB_MTSR_TEST_OFFSET_ADDR:
            return regs->mstr_test;
		case IPMB_DISCARD_CNT_OFFSET_ADDR:
            return regs->discard_cnt;
		case IPMB_RCV_CNT_OFFSET_ADDR:
            return regs->rcv_cnt;
		case IPMB_TX_STATE_OFFSET_ADDR:
            return regs->tx_state;
        default:
            return -1;
    }
    return ret;
}

static uint64_t hs0704_read_part2(hwaddr offset, hs0704_state *s)
{
    hs0704_regs *regs = &s->regs;
    uint32_t ret = 0;

    switch (offset) {
        case IPMB_ECC_ERR_INT_OFFSET_ADDR:
            return regs->ecc_err_int;
		case IPMB_START_TICK1_OFFSET_ADDR:
            return regs->start_tick1;
		case IPMB_START_TICK2_OFFSET_ADDR :
            return regs->start_tick2;
		case IPMB_STOP_TICK1_OFFSET_ADDR:
            return regs->stop_tick1;
		case IPMB_STOP_TICK2_OFFSET_ADDR:
            return regs->stop_tick2;
		case IPMB_WR_TICK1_OFFSET_ADDR:
            return regs->wr_tick1;
		case IPMB_WR_TICK2_OFFSET_ADDR:
            return regs->wr_tick2;
		case IPMB_RD_TICK1_OFFSET_ADDR:
            return regs->rd_tick1;
		case IPMB_RD_TICK2_OFFSET_ADDR:
            return regs->rd_tick2;
		case IPMB_IPMB_ID_OFFSET_ADDR:
            return regs->ipmb_id;
        default:
            return -1;
    }
    return ret;
}

static uint64_t hs0704_read(void *opaque, hwaddr offset, unsigned size)
{

    hs0704_state *s = (hs0704_state *)opaque;
    uint32_t ret = 0;


    if (offset <IPMB_PREP_LEN_OFFSET_ADDR) {
        ret =  hs0704_read_part0(offset, s);
    } else if ((offset >= IPMB_PREP_LEN_OFFSET_ADDR) && (offset <= IPMB_TX_STATE_OFFSET_ADDR)) {
        ret =  hs0704_read_part1(offset, s);
    }  else if ((offset > IPMB_TX_STATE_OFFSET_ADDR) && (offset <= IPMB_IPMB_ID_OFFSET_ADDR)) {
        ret =  hs0704_read_part2(offset, s);
    }  else {
        ret = -1;
    }

    DPRINTF("[i2c read reg]:0x%lx [offset:0x%x data:0x%x]\n", (uint64_t)opaque, (uint32_t)offset, (uint32_t)ret);
    return ret;
}

uint8_t* fifo8_to_array(Fifo8 *fifo, uint32_t *data_length){
	if (fifo == NULL || fifo8_is_empty(fifo)){
		return NULL;
	}

	uint32_t num = fifo8_num_used(fifo);
	*data_length = num;
	uint8_t *array = (uint8_t *)malloc(num * sizeof(uint8_t));
	if (array ==NULL) {
		return NULL;
	}

	uint32_t i = 0;
	while (!fifo8_is_empty(fifo)) {
		array[i] = fifo8_pop(fifo);
		i++;
	}

	return array;
}

static void ipmb_start_send(hs0704_state *s) {
	hs0704_regs *regs = &s->regs;
	uint32_t input_len;
	uint32_t output_len;
	uint8_t *input = fifo8_to_array(&s->tx_fifo, &input_len);
	uint8_t *output = imu_respond(input, &input_len, &output_len, &s->imu_info);

	fifo8_reset(&s->rx_fifo);
	fifo8_push_all(&s->rx_fifo, output, output_len);
	free(output);
	free(input);

	regs->sr |=BIT(1);
	qemu_set_irq(s->irq, 1);
}

static void clear_irq(hs0704_state *s){
	qemu_set_irq(s->irq, 0);
}

static void change_on_cr(uint64_t val, hs0704_state *s){
	if (val & BIT(0)){
		ipmb_start_send(s);
	}
	if (val & BIT(1)){
		clear_irq(s);
	}
	if (val & BIT(3)){
		fifo8_reset(&s->tx_fifo);
	}
}

static void hs0704_write_part0(hwaddr offset, uint64_t val, hs0704_state *s)
{
    hs0704_regs *regs = &s->regs;

    switch (offset) {
        case IPMB_CTR_OFFSET_ADDR:
            regs->ctr = val;
			break;
        case IPMB_TRXR_OFFSET_ADDR:
            fifo8_push(&s->tx_fifo, (uint8_t)val);
			break;
        case IPMB_CR_OFFSET_ADDR:
			regs->cr = val;
			change_on_cr(val, s);
            break;
        case IPMB_SR_OFFSET_ADDR:
            error_report("invalid write to sr");
			break;
        case IPMB_FRM_LEN_OFFSET_ADDR:
            regs->frm_len = val;break;
        case IPMB_RCV_LEN_OFFSET_ADDR:
			error_report("invalid write to rcv_num");
			break;
        case IPMB_TRANS_LEN_OFFSET_ADDR :
			error_report("invalid write to trans_num");
			break;
        default:
			break;
    }
}

static void hs0704_write_part1(hwaddr offset, uint64_t val, hs0704_state *s)
{
    hs0704_regs *regs = &s->regs;

    switch (offset) {
        case IPMB_PREP_LEN_OFFSET_ADDR:
            regs->prer = val;
			break;
		case IPMB_SLVADDR_A_OFFSET_ADDR:
            regs->slvaddr_a = val;
			break;
		case IPMB_SLVADDR_B_OFFSET_ADDR:
            regs->slvaddr_b = val;
			break;
		case IPMB_MTSR_TEST_OFFSET_ADDR:
            regs->mstr_test = val;
			break;
		case IPMB_DISCARD_CNT_OFFSET_ADDR:
            regs->discard_cnt = val;
			break;
		case IPMB_RCV_CNT_OFFSET_ADDR:
            regs->rcv_cnt = val;
			break;
		case IPMB_TX_STATE_OFFSET_ADDR:
            regs->tx_state = val;
			break;
        default:
			break;
    }
}

static void hs0704_write_part2(hwaddr offset, uint64_t val, hs0704_state *s)
{
    hs0704_regs *regs = &s->regs;

    switch (offset) {
        case IPMB_ECC_ERR_INT_OFFSET_ADDR:
            regs->ecc_err_int = val;
			break;
		case IPMB_START_TICK1_OFFSET_ADDR:
            regs->start_tick1 = val;
			break;
		case IPMB_START_TICK2_OFFSET_ADDR :
            regs->start_tick2 = val;
			break;
		case IPMB_STOP_TICK1_OFFSET_ADDR:
            regs->stop_tick1 = val;
			break;
		case IPMB_STOP_TICK2_OFFSET_ADDR:
            regs->stop_tick2 = val;
			break;
		case IPMB_WR_TICK1_OFFSET_ADDR:
            regs->wr_tick1 = val;
			break;
		case IPMB_WR_TICK2_OFFSET_ADDR:
            regs->wr_tick2 = val;
			break;
		case IPMB_RD_TICK1_OFFSET_ADDR:
            regs->rd_tick1 = val;
			break;
		case IPMB_RD_TICK2_OFFSET_ADDR:
            regs->rd_tick2 = val;
			break;
		case IPMB_IPMB_ID_OFFSET_ADDR:
            regs->ipmb_id = val;
			break;
        default:
			break;
    }
}

static void hs0704_write(void *opaque, hwaddr offset, uint64_t val, unsigned size)
{
    hs0704_state *s = (hs0704_state *)opaque;
    // DPRINTF("[i2c write reg]:0x%lx [offset:0x%x data:0x%x]\n", (uint64_t)opaque, (uint32_t)offset, (uint32_t)val);

    if (offset <IPMB_PREP_LEN_OFFSET_ADDR) {
        hs0704_write_part0(offset, val, s);
    } else if ((offset >= IPMB_PREP_LEN_OFFSET_ADDR) && (offset <= IPMB_TX_STATE_OFFSET_ADDR)) {
        hs0704_write_part1(offset, val, s);
    }  else if ((offset > IPMB_TX_STATE_OFFSET_ADDR) && (offset <= IPMB_IPMB_ID_OFFSET_ADDR)) {
        hs0704_write_part2(offset, val, s);
    }  else {
        DPRINTF("[i2c write reg]:offset:0x%x is false\n", offset);
    }
}

static const MemoryRegionOps hs0704_ops = {
    .read = hs0704_read,
    .write = hs0704_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
};

static void hs0704_reset(DeviceState *d)
{
    
	hs0704_state *s = HS0704(OBJECT(d));
    hs0704_regs *regs = &s->regs;
	memset(regs, 0, sizeof(hs0704_regs));
    fifo8_reset(&s->rx_fifo);
	fifo8_reset(&s->tx_fifo);

}

static void hs0704_init(Object *obj)
{
    DeviceState *dev = DEVICE(obj);
    SysBusDevice *sd = SYS_BUS_DEVICE(obj);
    hs0704_state *s = HS0704(obj);
	fifo8_create(&s->rx_fifo, FIFO_LENGTH);
	fifo8_create(&s->tx_fifo, FIFO_LENGTH);

    hs0704_regs *regs = &(s->regs);
    memset(regs, 0, sizeof(hs0704_regs));
    regs->slvaddr_a = 0x20;
    regs->slvaddr_b = 0x20;

    memory_region_init_io(&s->iomem, OBJECT(dev), &hs0704_ops, s, TYPE_HS0704_I2C, 0x1000);
    sysbus_init_mmio(sd, &s->iomem);
    sysbus_init_irq(sd, &s->irq);

	init_imu(IMU_MOCK_PATH, &s->imu_info);
	init_pcie_card(PCIE_FILE, &s->imu_info);
}

static void hs0704_realize(DeviceState *d, Error **errp)
{
}

static void hs0704_finalize(Object *obj)
{
}

static Property hs0704_properties[] = {
    DEFINE_PROP_END_OF_LIST(),
};

static void hs0704_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    dc->realize = hs0704_realize;
    dc->reset = hs0704_reset;
    device_class_set_props(dc, hs0704_properties);
}

static const TypeInfo hs0704_info = {
    .name          = TYPE_HS0704_I2C,
    .parent        = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(hs0704_state),
    .instance_init = hs0704_init,
    .instance_finalize = hs0704_finalize,
    .class_init    = hs0704_class_init,
};

static void hs0704_register_types(void)
{
    type_register_static(&hs0704_info);
}

type_init(hs0704_register_types)
